﻿using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using LINQPad.Extensibility.DataContext;
using mscoree;
using Xtensive.Orm;
using Xtensive.Orm.Configuration;
using Xtensive.Orm.Model;

namespace LINQPadDriver
{
  public class ModelCache : MarshalByRefObject
  {
    public static ModelCacheContainer GetModel(IConnectionInfo cxInfo)
    {
      string appDomainName = "XtensiveDomainCache" + cxInfo.AppConfigPath + cxInfo.DriverData;
      AppDomain appDomain = GetAppDomainByName(appDomainName);
      Assembly executingAssembly = Assembly.GetExecutingAssembly();
      if (appDomain==null) {
        var info = AppDomain.CurrentDomain.SetupInformation;
        info.ConfigurationFile = cxInfo.AppConfigPath;
        appDomain = AppDomain.CreateDomain(appDomainName, AppDomain.CurrentDomain.Evidence, info);
        DriverConfig config = DriverConfig.Get(cxInfo);
        appDomain.Load(File.ReadAllBytes(Assembly.GetExecutingAssembly().Location));
        appDomain.Load(File.ReadAllBytes((typeof (DynamicDataContextDriver)).Assembly.Location));
        appDomain.SetData("Config", config);
        appDomain.SetData("Paths", config.Assemblies.Select(Path.GetDirectoryName).Distinct().ToArray());
        appDomain.AssemblyResolve += AppDomainOnAssemblyResolve;
      }
      var cache =
        (ModelCache) appDomain.CreateInstanceAndUnwrap(executingAssembly.FullName, "LINQPadDriver.ModelCache");
      return cache.Get();
    }

    #region Non-public methods

    private static Assembly AppDomainOnAssemblyResolve(object sender, ResolveEventArgs args)
    {
      var name = new AssemblyName(args.Name);
      var paths = AppDomain.CurrentDomain.GetData("Paths") as string[];
      foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) {
        if (assembly.FullName==args.Name)
          return assembly;
      }
      if (paths!=null)
        foreach (string path in paths) {
          string path1 = Path.Combine(path, name.Name + ".dll");
          if (File.Exists(path1))
            return Assembly.Load(File.ReadAllBytes(path1));
          path1 = Path.Combine(path, name.Name + ".exe");
          if (File.Exists(path1))
            return Assembly.Load(File.ReadAllBytes(path1));
        }
      return null;
    }

    private static ModelCacheContainer _model;

    private ModelCacheContainer Get()
    {
      if (_model==null) {
        var driverConfig = (DriverConfig) AppDomain.CurrentDomain.GetData("Config");
        foreach (string a in driverConfig.Assemblies) {
          AppDomainOnAssemblyResolve(null, new ResolveEventArgs(AssemblyName.GetAssemblyName(a).FullName));
        }
        if (driverConfig.Mode==BuildMode.FromConfig) {
          string domainName = driverConfig.ConfigDomainName;
          DomainConfiguration config = DomainConfiguration.Load(domainName);
          Domain domain = Domain.Build(config);
          _model = new ModelCacheContainer(domain.Model);
        }
        else {
          string assembly = driverConfig.FactoryAssembly;
          string type = driverConfig.FactoryType;
          string method = driverConfig.FactoryMehod;
          MethodInfo methodInfo =
            AppDomain.CurrentDomain.GetAssemblies().Single(a => a.FullName==assembly).GetType(type).GetMethod(method);
          _model=new ModelCacheContainer(((Domain) methodInfo.Invoke(null, new object[0])).Model);
        }
      }
      return _model;
    }

    private static AppDomain GetAppDomainByName(string friendlyName)
    {
      IntPtr enumHandle = IntPtr.Zero;
      ICorRuntimeHost host = new CorRuntimeHost();
      try {
        host.EnumDomains(out enumHandle);

        object domain = null;
        while (true) {
          host.NextDomain(enumHandle, out domain);
          if (domain==null)
            break;
          var appDomain = (AppDomain) domain;
          if (appDomain.FriendlyName.Equals(friendlyName))
            return appDomain;
        }
      }
      finally {
        host.CloseEnum(enumHandle);
        Marshal.ReleaseComObject(host);
        host = null;
      }
      return null;
    }

    #endregion
  }
}